package com.googlecode.protoserv;

import com.google.protobuf.ByteString;
import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.Message;
import com.googlecode.protoserv.Protocol.ResponseProto;
import com.googlecode.protoserv.Protocol.RpcStatus;
import com.googlecode.protoserv.Transport.Encoding;

public class Rpc<ResponseType extends Message> {

  private int deadlineMs = Integer.MAX_VALUE;
  private Encoding encoding = Encoding.IDENTITY;
  private ByteString authentication;

  private GeneratedMessage.GeneratedExtension<ResponseProto, ResponseType> reponseExtension;
  private ResponseProto response;
  private RpcCallback<ResponseType> callback;

  public Rpc(RpcCallback<ResponseType> callback) {
    this.callback = callback;
  }

  public Rpc() {
    this(null);
  }

  /**
   * Sets amount of time server can spend answering request.
   * 
   * @param deadlineMs
   *          deadline in milliseconds.
   */
  public synchronized void setDeadlineMs(int deadlineMs) {
    this.deadlineMs = Math.max(0, deadlineMs);
  }

  /**
   * Returns deadline in ms for this call. If not specified, runtime will use
   * method's <code>(protoserv.deadline_ms)</code> defined at proto file.
   * Otherwise, will return runtime's default.
   * 
   * @return Deadline in ms for this call.
   */
  public synchronized int getDeadlineMs() {
    return deadlineMs;
  }

  /**
   * Tells whether request has deadline constraint.
   * 
   * @return True if deadline is set.
   */
  public synchronized boolean hasDeadlineMs() {
    return this.deadlineMs != Integer.MAX_VALUE;
  }

  /**
   * Sets encoding to be used at transport level while serializing request and
   * response messages.
   * 
   * @param encoding
   */
  public synchronized void setTransportEncoding(Encoding encoding) {
    this.encoding = encoding;
  }

  /**
   * Gets encoding to be used at transport level while serializing request and
   * response messages.
   * 
   * @return Transport encoding.
   */
  public synchronized Encoding getTransportEncoding() {
    return this.encoding;
  }

  /**
   * Sets authentication payload.
   * 
   * @param authentication
   */
  public void setAuthentication(ByteString authentication) {
    this.authentication = authentication;
  }

  /**
   * Returns authentication payload.
   * 
   * @return authentication payload.
   */
  public ByteString getAuthentication() {
    return this.authentication;
  }

  /**
   * Waits for RPC to finish for provided amount of time.
   * 
   * @param deadlineMs
   *          Amount of time to wait.
   * @return ResponseProto.Status.
   */
  public synchronized RpcStatus waitForResponse(int deadlineMs) {
    long stamp = System.currentTimeMillis();
    while (this.response == null) {
      long pastTime = System.currentTimeMillis() - stamp;
      if (deadlineMs > pastTime) {
        try {
          this.wait(deadlineMs - pastTime);
        } catch (InterruptedException e) {}
      } else {
        setResponse(RpcStatus.TIMEOUT, null);
      }
    }
    return this.response.getStatus();
  }

  /**
   * Waits for RPC to finish.
   * 
   * @return ResponseProto.Status.
   */
  public synchronized RpcStatus waitForResponse() {
    return waitForResponse(Integer.MAX_VALUE);
  }

  /**
   * Returns RPC response status. Returns <code>null</code> if response is not
   * yet available.
   * 
   * @return RPC response status.
   */
  public synchronized RpcStatus getStatus() {
    if (this.response != null) {
      return this.response.getStatus();
    }
    return null;
  }

  /**
   * Returns
   * 
   * @return
   */
  public synchronized ResponseType getResponse() {
    if (this.response != null) {
      return this.response.getExtension(this.reponseExtension);
    }
    return null;
  }

  // --------------------------------------------------------------------------
  // INTERNAL ONLY METHODS.
  // --------------------------------------------------------------------------

  public synchronized void setResponseExtension(
      GeneratedMessage.GeneratedExtension<ResponseProto, ResponseType> reponseExtension) {
    this.reponseExtension = reponseExtension;
  }

  public GeneratedMessage.GeneratedExtension<ResponseProto, ResponseType> getReponseExtension() {
    return reponseExtension;
  }

  synchronized RpcCallback<ResponseType> getCallback() {
    return callback;
  }

  public synchronized void setCallback(RpcCallback<ResponseType> callback) {
    synchronized (this) {
      this.callback = callback;
    }
  }

  // Assigns response.
  synchronized void setResponse(ResponseProto responseProto) {
    this.response = responseProto;
    if (this.callback != null) {
      try {
        this.callback.RpcDone(this);
      } catch (Throwable t) {
        t.printStackTrace();
      }
    }
    this.notifyAll();
  }

  // Builds response with a status.
  synchronized void setResponse(RpcStatus status, String message) {
    ResponseProto.Builder builder = ResponseProto.newBuilder();
    builder.setStatus(status);
    if (message != null) {
      builder.setStatusMessage(message);
    }
    setResponse(builder.build());
  }

  ///**
  //* Resets the RpcController to its initial state so that it may be reused in
  //* a new call.  This can be called from the client side only.  It must not
  //* be called while an RPC is in progress.
  //*/
  //void reset();
  //
  ///**
  //* After a call has finished, returns true if the call failed.  The possible
  //* reasons for failure depend on the RPC implementation.  {@code failed()}
  //* most only be called on the client side, and must not be called before a
  //* call has finished.
  //*/
  //boolean failed();
  //
  ///**
  //* If {@code failed()} is {@code true}, returns a human-readable description
  //* of the error.
  //*/
  //String errorText();
  //
  ///**
  //* Advises the RPC system that the caller desires that the RPC call be
  //* canceled.  The RPC system may cancel it immediately, may wait awhile and
  //* then cancel it, or may not even cancel the call at all.  If the call is
  //* canceled, the "done" callback will still be called and the RpcController
  //* will indicate that the call failed at that time.
  //*/
  //void startCancel();
  //
  //// -----------------------------------------------------------------
  //// These calls may be made from the server side only.  Their results
  //// are undefined on the client side (may throw RuntimeExceptions).
  //
  ///**
  //* Causes {@code failed()} to return true on the client side.  {@code reason}
  //* will be incorporated into the message returned by {@code errorText()}.
  //* If you find you need to return machine-readable information about
  //* failures, you should incorporate it into your response protocol buffer
  //* and should NOT call {@code setFailed()}.
  //*/
  //void setFailed(String reason);
  //
  ///**
  //* If {@code true}, indicates that the client canceled the RPC, so the server
  //* may as well give up on replying to it.  This method must be called on the
  //* server side only.  The server should still call the final "done" callback.
  //*/
  //boolean isCanceled();
  //
  ///**
  //* Asks that the given callback be called when the RPC is canceled.  The
  //* parameter passed to the callback will always be {@code null}.  The
  //* callback will always be called exactly once.  If the RPC completes without
  //* being canceled, the callback will be called after completion.  If the RPC
  //* has already been canceled when NotifyOnCancel() is called, the callback
  //* will be called immediately.
  //*
  //* <p>{@code notifyOnCancel()} must be called no more than once per request.
  //* It must be called on the server side only.
  //*/
  //void notifyOnCancel(RpcCallback<Object> callback);
}
